# Copyright 2008 Clint Banis.
# All rights reserved.
#
#    Draws a daily affirmation picture with tribe and tone glyphs and
#    affirmation text.
#
#    The Glyph class organizes the operations against folders.
#    Operations include:
#        - Generating a wallpaper image for the day.
#        - Setting the wallpaper via the Windows Shell API.
#        - Managing generated files.
#
#  --
#
# GENERAL DESIGN:
#        - Object oriented drawing
#        - Paints the tone and tribe glyphs, the fully toned name of the day,
#          and the lines of the daily affirmation across the bottom.
#        - Allows soft backgrounds and a highlighted time prompt showing the
#          Gregarian Calendar day.
#        - Font draw resizing.
#
#  --
#
# TODO:
#        - Download new font (papyrus)
#        - Larger font size
#
#        - Site specific path configuration: this will befound in script.Application
#
from os.path import join as joinpath, exists
from os import listdir

from PIL.ImageFont import truetype

# Trying to use a mkdir variant that makes subdirectories.
try: from os import mkdirs
except ImportError:
    def mkdirs(d):
        from os import mkdir
        return mkdir(d)

from pdb import set_trace as debug

from mayan_cal import GALACTIC_CONSTANT # Reference here because we're insecure.
from mayan_cal.affirmation import Affirmation, daily_affirmation, todays_affirmation

class COLOR:
    BLACK     = (0,   0,   0  )

    WHITE     = (255, 255, 255)
    RED       = (255, 0,   0  )
    BLUE      = (0,   0,   255)
    YELLOW    = (255, 255, 0  )

    CYAN      = (0,   255, 255)

    WHITE_BG  = (225, 225, 240)
    RED_BG    = (255, 165, 165)
    BLUE_BG   = (100, 100, 250)
    YELLOW_BG = (100, 225, 225)

    # Opposites
    NORTH_BG  = BLUE_BG
    EAST_BG   = YELLOW_BG
    WEST_BG   = WHITE_BG
    SOUTH_BG  = RED_BG

    NORTH_FILL = (55,  55,  250)
    EAST_FILL  = (0,   200, 200)
    WEST_FILL  = (225, 225, 240)
    SOUTH_FILL = (225, 50,  50)

    ##    MAGENTA   = (200, 200, 0  ) # XXX
    ##    DATE_STR_FILL = MAGENTA

class Backend(object):
    # All Wallpaper instances are tied to a backend object for loading
    # and saving.
    #
    # The Backend is abstract, in that is must have its FOLDERS attribute
    # configured as an object containing TRIBE_INPUT, TONE_INPUT, and OUTPUT
    # paths.
    #
    # The Backend simply represents the input source for all drawings and
    # configuration.
    #
    INPUT_SUFFIXES = ('png', 'jpg', 'jpeg')
    OUTPUT_SUFFIX = 'png'

    class AbstractStateError(Exception):
        pass

    class Folders:
        BASE        = joinpath('.') # ('mayan_cal')

        GLYPHS      = joinpath(BASE,   'glyphs')
        TRIBE_INPUT = joinpath(GLYPHS, 'tribes')
        TONE_INPUT  = joinpath(GLYPHS, 'tones')

        OUTPUT      = joinpath('affirmations', 'daily')

        FONTFILE_PATH = joinpath(GLYPHS, 'pilfont')

        def __init__(self, output_dir = None, glyph_dir = None,
                           tribe_dir  = None, tone_dir  = None,
                           fontfile_path = None):

            # First, program the output dir, and the tribe and tone
            # directories separately.
            if output_dir:
                self.OUTPUT = output_dir
            if tribe_dir:
                self.TRIBE_INPUT = tribe_dir
            if tone_dir:
                self.TONE_INPUT = tone_dir
            if glyph_dir:
                self.GLYPHS = glyph_dir

                # If the glyph dir is set, then the tribe and tone
                # folders are appended instead, overriding.
                self.TRIBE_INPUT = joinpath(glyph_dir, tribe_dir or 'tribes')
                self.TONE_INPUT  = joinpath(glyph_dir, tone_dir  or 'tones' )
            if fontfile_path:
                self.FONTFILE_PATH = fontfile_path

            self.verify_state()

        def verify_state(self):
            # Move into FOLDERS implementation.
            for attr in ('TRIBE_INPUT', 'TONE_INPUT', 'OUTPUT', 'FONTFILE_PATH'):
                assert hasattr(self, attr), \
                        self.AbstractStateError('%s not set!' % attr)

    def __init__(self, **kwd):
        self.folders = (kwd)

    def get_folders(self):
        return getattr(self, '_folders', None)
    def set_folders(self, kwd):
        self._folders = Backend.Folders(**kwd)

    folders = FOLDERS = property(get_folders, set_folders)

    def search_filename(self, name, input_folder, suffixes = None):
        # This seems complicated, but is supposed to lend flexibility in
        # searching insensitively to case and with naming the input folder.
        # Note this also strips all spaces from name matching.
        # XXX Document this function better.
        if suffixes is None:
            suffixes = self.INPUT_SUFFIXES

        processed_name = name.lower().replace(' ', '')
        for candidate in listdir(input_folder):
            processed_candidate = candidate.lower().replace(' ', '')
            if processed_candidate.startswith(processed_name):
                for suffix in suffixes:
                    if '%s.%s' % (processed_name, suffix) == processed_candidate:
                        return joinpath(input_folder, candidate)

        raise NameError(name) # XXX Should raise OSError(errno = errno.ENOENT)

    def get_tribe_glyph_filename(self, tribe):
        return self.search_filename(tribe.name, self.FOLDERS.TRIBE_INPUT)
    def get_tone_glyph_filename(self, tone):
        return self.search_filename(tone.name, self.FOLDERS.TONE_INPUT)

    # Do I want to support this?
    def get_wallpaper(self, factory, day):
        return factory(day, backend = self)

    # Image operations.
    def load_tribe_glyph(self, tribe):
        # Load and return an image based on tribe.
        return load_glyph(self.get_tribe_glyph_filename(tribe))

    def load_tone_glyph(self, tone):
        # Load and return an image based on the tone.
        return load_glyph(self.get_tone_glyph_filename(tone))

    def load_font(self, filename = None, size = 12):
        try: return truetype(filename or self.FOLDERS.FONTFILE_PATH, size)
        except IOError, e:
            from errno import ENOENT
            # The message is set by the imaging library.
            if e.errno != ENOENT and e.message != 'cannot open resource':
                raise

    # PIL operation.
    def save_image(self, im, filename):
        if not exists(self.FOLDERS.OUTPUT):
            mkdirs(self.FOLDERS.OUTPUT)

        path = joinpath(self.FOLDERS.OUTPUT, filename)
        im.save(path)

        from mayan_cal.desktop import file_checksum
        self.last_checksum = file_checksum(path)

class Wallpaper:
    # The actual drawing and saving is separated from this class and
    # found in the module namespace (implemented via CANVAS).
    #
    # Property descriptors tend to cache attributes like picture.
    def __init__(self, day, backend = None, **kwd):
        assert isinstance(day, Affirmation)
        self.backend = backend
        self.day = day
        self.__dict__.update(kwd)

    # Assembly shortcuts.
    def get_title(self):
        return '%s %s' % (self.day.tone.name, self.day.tribe.name)
    def get_affirmation_lines(self):
        return affirmation_lines(self.day)

    title = property(get_title)
    __iter__ = get_affirmation_lines

    def load_tribe_glyph(self):
        return self.backend.load_tribe_glyph(self.day.tribe)
    def load_tone_glyph(self):
        return self.backend.load_tone_glyph(self.day.tone)

    tribe_glyph = property(load_tribe_glyph)
    tone_glyph  = property(load_tone_glyph)

    def get_tribe_background(self):
        # Should this be rendered from the CANVAS?
        if hasattr(self, 'bgcolor'):
            return self.bgcolor

        # Case sensitive!
        return CANVAS.BACKGROUNDS.get(self.day.tribe.color.color)

    tribe_background = property(get_tribe_background)

    def get_prompt_color(self):
        return CANVAS.TIME_PROMPT.get(self.day.tribe.color.color)

    def load_picture(self):
        # Call the global routine to do the actual drawing.
        d = self.day.day
        p = d.strftime
        time_prompt = p('%%A, %%B %s%s %%Y' % (p('%e').lstrip(), th(d.day)))

        return CANVAS(self.backend.load_font(),
                      self.get_tribe_background(),
                      (self.get_title(),
                       self.get_affirmation_lines()),
                      (self.load_tribe_glyph(),
                       self.load_tone_glyph()),
                      time_prompt,
                      self.get_prompt_color())

    def get_picture(self):
        try: return self._picture
        except AttributeError:
            pict = self._picture = self.load_picture()

        return pict

    def del_picture(self):
        try: del self._picture
        except AttributeError: pass

    picture = property(fget = get_picture, fdel = del_picture)

    def get_filename(self):
        return 'MayanWallpaper-%s.%s' % (self.day.day.strftime('%b%dth%Y'),
                                         self.backend.OUTPUT_SUFFIX)

    @property
    def full_path(self):
        return joinpath(self.backend.FOLDERS.OUTPUT, self.filename)

    filename = property(get_filename)

    def save_image(self, im):
        return self.backend.save_image(im, self.get_filename())

    @property
    def saved(self):
        im = self.load_picture()
        self.save_image(im)
        return im

    @property
    def full_saved_path(self):
        (self.saved)
        return self.full_path

    def install_desktop(self):
        # Install based on module implementation.
        from mayan_cal.desktop import install_wallpaper
        return install_wallpaper(wallpaper = self)

# Move this into affirmations.py?
def affirmation_lines(af):
    line = []
    for part in af:
        if part == '\n':
            yield ' '.join(line)
            line = []
        else:
            line.append(part)

    if line:
        yield ' '.join(line)


# Implementation of drawing.
from PIL import Image, ImageDraw, ImageFont
def load_glyph(filename):
    return Image.open(filename)

X, Y = W, H = 0, 1
class CANVAS(object):
    # Opposites.
    BACKGROUNDS = {'White'  : COLOR.NORTH_BG,        'North'  : COLOR.NORTH_BG,
                   'Red'    : COLOR.EAST_BG,         'East'   : COLOR.EAST_BG,
                   'Blue'   : COLOR.WEST_BG,         'West'   : COLOR.WEST_BG,
                   'Yellow' : COLOR.SOUTH_BG,        'South'  : COLOR.SOUTH_BG}

    TIME_PROMPT = {'White'  : COLOR.NORTH_FILL,      'North'  : COLOR.NORTH_FILL,
                   'Red'    : COLOR.EAST_FILL,       'East'   : COLOR.EAST_FILL,
                   'Blue'   : COLOR.WEST_FILL,       'West'   : COLOR.WEST_FILL,
                   'Yellow' : COLOR.SOUTH_FILL,      'South'  : COLOR.SOUTH_FILL}

    SIZE   = (400, 350)
    CENTER = (SIZE[X] / 2, SIZE[X] / 2)

    TONE_NEWSIZE  = (85, 165)
    TRIBE_NEWSIZE = (200, 200)

    TRANSPARENT = COLOR.WHITE

    class PADDING:
        class GLYPH:
            XAXIS = 10
        class TEXT:
            XAXIS = 4
            YAXIS = 4

    TRIBE_OFFSET = (CENTER[X] - \
                    (TRIBE_NEWSIZE[X] / 2) + \
                    int(TRIBE_NEWSIZE[X] / 2 * .1), \
                    40)

    TONE_OFFSET  = (TRIBE_OFFSET[X] - TONE_NEWSIZE[X] - PADDING.GLYPH.XAXIS, 55)

    class TITLE_OFFSET:
        YAXIS = 15

    class AFFIRMATION_OFFSET:
        YAXIS = 255

    def __new__(self, *args, **kwd):
        i = object.__new__(self)
        return i.draw_affirmation(*args, **kwd)

    # Object instance methods.
    def draw_affirmation(self, font, bgcolor, (title, affirmation),
                         (tribe_glyph, tone_glyph), time_prompt, prompt_fill):

        # Assemble a new graphic with the tribe and tone glyphs, and
        # drawing the affirmation text around it.
        #
        # Eventually this should go into the class as a certain picture
        # method.  Right now its down here because of its reliance on PIL.

        # Create a new canvas.
        im = Image.new('RGB', self.SIZE, bgcolor)

        # Draw the tone glyph center, left, transparently.
        tone_glyph = tone_glyph.resize(self.TONE_NEWSIZE)

        # Generate transparency mask from tone_glyph.
        mask = transparent_mask(tone_glyph)
        im.paste(tone_glyph, box = self.TONE_OFFSET, mask = mask)

        # Draw the tribe glyph, center, overlapping.
        tribe_glyph = tribe_glyph.resize(self.TRIBE_NEWSIZE)
        im.paste(tribe_glyph, box = self.TRIBE_OFFSET)

        # Create drawing context for text.
        draw = ImageDraw.Draw(im)

        # Draw the title string, top, overlapping.
        # Todo: Calculate center alignment.
        draw.text((self.center_text_xaxis(draw, title), self.TITLE_OFFSET.YAXIS),
                  title, fill = COLOR.BLACK, font = font)

        # Draw the affirmation lines, bottom spanning vertical, overlapping.
        # Todo: Calculate center alignment.
        yaxis = self.AFFIRMATION_OFFSET.YAXIS
        for line in affirmation:
            draw.text((self.center_text_xaxis(draw, line), yaxis),
                      line, fill = COLOR.BLACK, font = font)

            yaxis += draw.textsize(line)[Y] + self.PADDING.TEXT.YAXIS # Padding

        metrics = draw.textsize(time_prompt)
        draw.text((self.TRIBE_OFFSET[X] + self.TRIBE_NEWSIZE[X] \
                       - metrics[X] - self.PADDING.TEXT.XAXIS * 2,

                   self.TRIBE_OFFSET[Y] + self.TRIBE_NEWSIZE[Y] \
                       - metrics[Y] - self.PADDING.TEXT.YAXIS * 2),

                  time_prompt, fill = prompt_fill, font = font)

        return im

    def center_text_xaxis(self, draw, text):
        return self.CENTER[X] - (draw.textsize(text)[X] / 2)

    def draw_resized_text(self, text, im, box, font = None, bgcolor = None):
        # Emulate font-sizing by doing PIL resize and paste.
        if font is None:
            import ImageFont
            font = ImageFont.load_default()

        # Draw into a temporary box.  (padding?)
        # bgcolor - maybe use an alpha-friendly blend against original image?
        tmp = Image.new(im.mode, font.getsize(text))

        draw = ImageDraw.Draw(tmp)
        draw.text((0, 0), text)

        # Resize the temporary image to the dimensions given by the box.
        fullsize = (box[2]-box[0], box[3]-box[1])
        tmp.resize(fullsize) # ANTIALIAS?

        # Now paste it into the bounding-box on the original image.
        im.paste(tmp, box = box)

def transparent_mask(glyph, bgcolor = CANVAS.TRANSPARENT, opacity = 255):
    # This works too!
    if glyph.mode != 'RGBA':
        glyph = glyph.convert('RGBA')
    # XXX Otherwise, modify THIS image's alpha band.

    def alphaMask((r, g, b, a)):
        if (r, g, b) == bgcolor:
            return 0

        # XXX I'm not even really sure providing an opacity here
        # will effect the transparency.
        return opacity

    alpha = Image.new('1', glyph.size)
    alpha.putdata(map(alphaMask, glyph.getdata()))
    glyph.putalpha(alpha)
    return glyph

def saveImageTo(original, destination):
    # Suggest mode via filename.
    Image.open(original).save(destination)
    return destination

def th(day):
    return bool(int(day / 10) != 1) \
           and {1: 'st', 2: 'nd', 3: 'rd'}.get(day % 10) \
           or 'th'

# XXX Local Application.
Store = Backend()

class Today(Wallpaper):
    def __init__(self, backend = Store):
        Wallpaper.__init__(self, todays_affirmation(), backend = backend)

    def get_filename(self):
        return 'today.%s' % self.backend.OUTPUT_SUFFIX

# Day passed is an affirmation.
def draw(day = None, backend = Store, wallpaper = None):
    # A wallpaper is necessary to return the saved (complete) state.
    # It will default to the day.  backend is necessary.
    if wallpaper is None:
        wallpaper = day is None and Today or Wallpaper(day, backend = backend)

    # Load Picture from Canvas and save to outgoing directory.
    # Return filename?
    return wallpaper.saved

# Draw a full calendar month.
# Todo: parameterized font.
def month_days(year, month):
    from mayan_cal import gregarian_date
    try:
        for day in xrange(1, 32):
            yield gregarian_date(year, month, day)

    except ValueError:
        pass

def daily_month_image(day, backend = Store, **kwd):
    wallpaper = Wallpaper(daily_affirmation(day), backend = backend, **kwd)

    # Force wallpaper to delete stored picture because we're doing bad things to it.
    im = wallpaper.picture
    try: del wallpaper.picture
    except AttributeError: pass

    return im.resize((MONTHDAY_WIDTH, MONTHDAY_HEIGHT)), wallpaper

RESOLUTION = (1280, 1024)

MONTHDAY_WIDTH  = RESOLUTION[X] / 7
MONTHDAY_HEIGHT = RESOLUTION[Y] / 5

def month_day_images(year, month, backend = Store, **kwd):
    for day in month_days(year, month):
        im, wallpaper = daily_month_image(day, backend = backend, **kwd)
        yield (day, im, wallpaper.day.tone.name + ' ' + wallpaper.day.tribe.name)

def draw_month_images(images):
    canvas = Image.new('RGB', RESOLUTION, COLOR.WHITE)

    x = y = w = 0
    for (day, im, name) in images:
        w += 1 # Todo: get weekday name.

        print '#%3d' % w, 'Pasting [%s] %-35s' % (day, name), 'Into (%3d, %3d)' % (x, y)
        canvas.paste(im, box = (x, y))

        if 0 == (day.day % 7):
            # End of week.
            x = 0
            y += MONTHDAY_HEIGHT

        else:
            x += MONTHDAY_WIDTH

        if w == 7:
            w = 0

    month_name = day.strftime('%B %Y')

    draw = ImageDraw.Draw(canvas)
    w, h = draw.textsize(month_name)

    draw.text((canvas.size[X] - w - 8, canvas.size[Y] - h - 8), month_name, fill = COLOR.BLACK)
    return canvas

def draw_month(year, month, backend = Store):
    return draw_month_images(month_day_images(year, month, backend = backend))

def draw_year(year, backend = Store):
    for month in xrange(1, 13):
        yield (month, draw_month(year, month))

# Annual calendar wallpapers -- These backgrounds are indexed later in the procedure.
TRIBE_BACKGROUNDS = [COLOR.RED_BG, COLOR.WHITE_BG, COLOR.BLUE_BG, COLOR.YELLOW_BG]
TRIBE_OFFSET = ['Red', 'White', 'Blue', 'Yellow'].index

# Capable of starting from a single index.
def cycle_color(start, bg = TRIBE_BACKGROUNDS):
    while True:
        for color in bg[start:]:
            yield color
        for color in bg[:start]:
            yield color

def draw_galactic_year(new_day, backend = Store):
    canvas = Image.new('RGB', (MONTHDAY_WIDTH * 13, MONTHDAY_HEIGHT * 20), COLOR.WHITE)

    x = y = i = 0
    # Todo: Start iterating from new_day.{day, month, year}
    for m in xrange(1, 13):
        if i >= GALACTIC_CONSTANT:
            break # For sho.

        for day in month_days(new_day.year, m):
            i += 1

            if i == 1:
                # On the very first iteration, we temporarily construct an affirmation
                # simply to gain its kin-qualified tribe color to start the cycle.
                offset = TRIBE_OFFSET(daily_affirmation(day).tribe.color.color)
                next_bgcolor = cycle_color(offset).next

            im, wallpaper = daily_month_image(day, backend = backend,
                                              bgcolor = next_bgcolor())

            name = wallpaper.day.tone.name + ' ' + wallpaper.day.tribe.name
            print '#%3d' % i, 'Pasting [%s] %-35s' % (day, name), 'Into (%3d, %3d)' % (x, y)

            canvas.paste(im, box = (x * MONTHDAY_WIDTH, y * MONTHDAY_HEIGHT))

            if y == 19:
                y = 0
                x += 1
            else:
                y += 1

            if x == 13:
                x = 0

    return canvas

def draw_text_lines(draw, origin, text,
                    padding = CANVAS.PADDING.TEXT.YAXIS,
                    color = COLOR.BLACK):
    y = 0

    for line in text.split('\n'):
        draw.text((origin[X], origin[Y] + y), line, fill = color)
        y += draw.textsize(line)[Y] + padding

def draw_galactic_info_chart(new_day, backend = Store):
    # Pass the FIRST day of the year!
    canvas = Image.new('RGB', (1000, 800), color = COLOR.WHITE)

    HEIGHT = 700
    OFFSET = 50

    # Left pane - Draw full calendar year.
    im = draw_galactic_year(new_day, backend = backend)

    ratio = (float(HEIGHT) / im.size[Y])
    resizex = int(ratio * im.size[X])

    im = im.resize((resizex, HEIGHT))
    canvas.paste(im, box = (OFFSET, OFFSET, im.size[X] + OFFSET, im.size[Y] + OFFSET))

    # New vertical alignment.
    rightpaneleft = resizex + (OFFSET * 2)

    # Right pane - Text about first date of year.
    draw = ImageDraw.Draw(canvas)

    text = new_day.strftime('%B %d' + th(new_day.day) + ' %Y')
    text += ' - First day of the New Year'
    text += '\n'
    text += daily_affirmation(new_day).tribe.color.season.name

    draw_text_lines(draw, (rightpaneleft, OFFSET), text, color = COLOR.BLACK)

    # New vertical alignment.
    glyphposy = (draw.textsize(text)[Y] * 2) + (OFFSET * 2)

    # Draw wallpaper glyph.
    im = Wallpaper(daily_affirmation(new_day), backend = backend).picture
    im = im.resize((400, int((400.0 / im.size[X]) * im.size[Y])))

    canvas.paste(im, box = (rightpaneleft , glyphposy ,
                            rightpaneleft + im.size[X],
                            glyphposy     + im.size[Y]))


    undertextposy = glyphposy + im.size[Y] + (OFFSET * 4)
    draw.text((rightpaneleft, undertextposy),
              'Destiny Oracle', fill = COLOR.BLACK)

    return canvas

def watermark_wallpaper(image, watermark = None, box = (),
                        bgcolor = CANVAS.TRANSPARENT, opacity = 255):

    # Resize to box dimensions.
    size = (box[2] - box[0], box[3] - box[1])
    wm = watermark.resize(size)

    # Make the background color transparent.
    mask = transparent_mask(wm, bgcolor = bgcolor, opacity = opacity)

    # Paste the wallpaper glyph translucently onto original image.
    image.paste(wm, box = box, mask = mask)
    return image

def getbbox((x, y), (w, h)):
    return (x, y, x + w, y + h)

def watermark_resolution(image, watermark = None,
                         resolution = (None, None),
                         width = None, height = None,
                         **kwd):
    # From upper right.
    OFFSET = 15

    box = getbbox((resolution[X] - width - OFFSET, OFFSET), (width, height))
    return watermark_wallpaper(image, watermark = watermark, box = box, **kwd)
 
def compositeWatermark(wallpaper, watermark, resolution, output, **kwd):
    wallpaper_im = Image.open(wallpaper).resize(resolution)
    watermark_im = Image.open(watermark)

    width  = kwd.get('width',  200)
    height = kwd.get('height', 180)

    try: del kwd['width']
    except KeyError: pass
    try: del kwd['height']
    except KeyError: pass

    composite = watermark_resolution(wallpaper_im, watermark = watermark_im,
                                     resolution = resolution,
                                     width = width, height = height,
                                     **kwd)
    composite.save(output)
    return composite

def draw_destiny_castle(day, bgcolor = COLOR.WHITE):
    # XXX Todo: bypass affirmations and use calculate functions directly.
    af = daily_affirmation(day)
    tribe, tone = af.tribe, af.tone

    canvas = Image.new('RGB', 400, 300, bgcolor = bgcolor)

    # Draw star-mark.

    # Draw containing spirals.

    return canvas
